/***************************************************************************
 *   Copyright (c) 2022 Abdullah Tahiri <abdullah.tahiri.yo@gmail.com>     *
 *                                                                         *
 *   This file is part of the FreeCAD CAx development system.              *
 *                                                                         *
 *   This library is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Library General Public           *
 *   License as published by the Free Software Foundation; either          *
 *   version 2 of the License, or (at your option) any later version.      *
 *                                                                         *
 *   This library  is distributed in the hope that it will be useful,      *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU Library General Public License for more details.                  *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this library; see the file COPYING.LIB. If not,    *
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
 *   Suite 330, Boston, MA  02111-1307, USA                                *
 *                                                                         *
 ***************************************************************************/

#ifndef SKETCHERGUI_DrawSketchHandlerBSpline_H
#define SKETCHERGUI_DrawSketchHandlerBSpline_H

#include <Inventor/events/SoKeyboardEvent.h>
#include <QInputDialog>

#include <Gui/Notifications.h>
#include <Gui/Command.h>
#include <Gui/CommandT.h>

#include <Mod/Sketcher/App/SketchObject.h>

#include "DrawSketchHandler.h"
#include "GeometryCreationMode.h"
#include "Utils.h"
#include "ViewProviderSketch.h"


namespace SketcherGui
{

extern GeometryCreationMode geometryCreationMode;  // defined in CommandCreateGeo.cpp

class DrawSketchHandlerBSpline: public DrawSketchHandler
{
public:
    explicit DrawSketchHandlerBSpline(int constructionMethod)
        : Mode(STATUS_SEEK_FIRST_CONTROLPOINT)
        , MousePressMode(MOUSE_NOT_PRESSED)
        , ConstrMethod(constructionMethod)
        , SplineDegree(3)
        , IsClosed(false)
    {
        addSugConstraint();
        applyCursor();
    }

    ~DrawSketchHandlerBSpline() override = default;

    /// modes
    enum SELECT_MODE
    {
        STATUS_SEEK_FIRST_CONTROLPOINT,
        STATUS_SEEK_ADDITIONAL_CONTROLPOINTS,
        STATUS_CLOSE
    };

    // TODO: this kind of behavior will be useful in a superclass
    // when LMB is pressed it's a transitional state so some undos can't be done
    // (like delete last pole)
    enum MOUSE_PRESS_MODE
    {
        MOUSE_PRESSED,
        MOUSE_NOT_PRESSED
    };

    void mouseMove(Base::Vector2d onSketchPos) override
    {
        prevCursorPosition = onSketchPos;

        if (Mode == STATUS_SEEK_FIRST_CONTROLPOINT) {
            setPositionText(onSketchPos);

            if (seekAutoConstraint(sugConstr.back(), onSketchPos, Base::Vector2d(0.f, 0.f))) {
                renderSuggestConstraintsCursor(sugConstr.back());
                return;
            }
        }
        else if (Mode == STATUS_SEEK_ADDITIONAL_CONTROLPOINTS) {

            drawBSplineToPosition(onSketchPos);

            drawCursorToPosition(onSketchPos);

            if (seekAutoConstraint(sugConstr.back(), onSketchPos, Base::Vector2d(0.f, 0.f))) {
                renderSuggestConstraintsCursor(sugConstr.back());
                return;
            }
        }
    }

    bool pressButton(Base::Vector2d onSketchPos) override
    {
        prevCursorPosition = onSketchPos;

        MousePressMode = MOUSE_PRESSED;

        if (Mode == STATUS_SEEK_FIRST_CONTROLPOINT) {
            BSplinePoles.push_back(onSketchPos);

            Mode = STATUS_SEEK_ADDITIONAL_CONTROLPOINTS;

            // insert circle point for pole, defer internal alignment constraining.
            try {
                Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add Pole circle"));

                // Add pole
                Gui::cmdAppObjectArgs(
                    sketchgui->getObject(),
                    "addGeometry(Part.Circle(App.Vector(%f,%f,0),App.Vector(0,0,1),10),True)",
                    BSplinePoles.back().x,
                    BSplinePoles.back().y);

                poleGeoIds.push_back(getHighestCurveIndex());

                Gui::cmdAppObjectArgs(sketchgui->getObject(),
                                      "addConstraint(Sketcher.Constraint('Weight',%d,%f)) ",
                                      poleGeoIds.back(),
                                      1.0);  // First pole defaults to 1.0 weight
            }
            catch (const Base::Exception&) {
                Gui::NotifyError(sketchgui,
                                 QT_TRANSLATE_NOOP("Notifications", "Error"),
                                 QT_TRANSLATE_NOOP("Notifications", "Error adding B-Spline pole"));

                Gui::Command::abortCommand();

                static_cast<Sketcher::SketchObject*>(sketchgui->getObject())->solve();

                return false;
            }

            // Gui::Command::commitCommand();

            // static_cast<Sketcher::SketchObject *>(sketchgui->getObject())->solve();

            // add auto constraints on pole
            if (!sugConstr.back().empty()) {
                createAutoConstraints(sugConstr.back(),
                                      poleGeoIds.back(),
                                      Sketcher::PointPos::mid,
                                      false);
            }

            static_cast<Sketcher::SketchObject*>(sketchgui->getObject())->solve();

            addSugConstraint();
        }
        else if (Mode == STATUS_SEEK_ADDITIONAL_CONTROLPOINTS) {
            BSplinePoles.push_back(onSketchPos);

            // check if coincident with first pole
            for (auto& ac : sugConstr.back()) {
                if (ac.Type == Sketcher::Coincident) {
                    if (ac.GeoId == poleGeoIds[0] && ac.PosId == Sketcher::PointPos::mid) {
                        IsClosed = true;
                    }
                    else {
                        // The coincidence with first point may be indirect
                        const auto coincidents =
                            static_cast<Sketcher::SketchObject*>(sketchgui->getObject())
                                ->getAllCoincidentPoints(ac.GeoId, ac.PosId);
                        if (coincidents.find(poleGeoIds[0]) != coincidents.end()
                            && coincidents.at(poleGeoIds[0]) == Sketcher::PointPos::mid) {
                            IsClosed = true;
                        }
                    }
                }
            }

            if (IsClosed) {
                Mode = STATUS_CLOSE;

                if (ConstrMethod == 1) {  // if periodic we do not need the last pole
                    BSplinePoles.pop_back();
                    sugConstr.pop_back();

                    return true;
                }
            }

            // insert circle point for pole, defer internal alignment constraining.
            try {

                // Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add Pole circle"));

                // Add pole
                Gui::cmdAppObjectArgs(
                    sketchgui->getObject(),
                    "addGeometry(Part.Circle(App.Vector(%f,%f,0),App.Vector(0,0,1),10),True)",
                    BSplinePoles.back().x,
                    BSplinePoles.back().y);

                poleGeoIds.push_back(getHighestCurveIndex());

                Gui::cmdAppObjectArgs(sketchgui->getObject(),
                                      "addConstraint(Sketcher.Constraint('Equal',%d,%d)) ",
                                      poleGeoIds[0],
                                      poleGeoIds.back());
            }
            catch (const Base::Exception&) {
                Gui::NotifyError(
                    sketchgui,
                    QT_TRANSLATE_NOOP("Notifications", "Error"),
                    QT_TRANSLATE_NOOP("Notifications", "Error creating B-spline pole"));
                Gui::Command::abortCommand();

                static_cast<Sketcher::SketchObject*>(sketchgui->getObject())->solve();

                return false;
            }

            // Gui::Command::commitCommand();

            // static_cast<Sketcher::SketchObject *>(sketchgui->getObject())->solve();

            // add auto constraints on pole
            if (!sugConstr.back().empty()) {
                createAutoConstraints(sugConstr.back(),
                                      poleGeoIds.back(),
                                      Sketcher::PointPos::mid,
                                      false);
            }

            // static_cast<Sketcher::SketchObject *>(sketchgui->getObject())->solve();

            if (!IsClosed) {
                addSugConstraint();
            }
        }
        return true;
    }

    bool releaseButton(Base::Vector2d onSketchPos) override
    {
        prevCursorPosition = onSketchPos;
        MousePressMode = MOUSE_NOT_PRESSED;

        return finishCommand(onSketchPos);
    }

    void registerPressedKey(bool pressed, int key) override
    {
        if (SoKeyboardEvent::D == key && pressed) {
            SplineDegree =
                QInputDialog::getInt(Gui::getMainWindow(),
                                     QObject::tr("B-Spline Degree"),
                                     QObject::tr("Define B-Spline Degree, between 1 and %1:")
                                         .arg(QString::number(Geom_BSplineCurve::MaxDegree())),
                                     SplineDegree,
                                     1,
                                     Geom_BSplineCurve::MaxDegree(),
                                     1);
            // FIXME: Pressing Esc here also finishes the B-Spline creation.
            // The user may only want to exit the dialog.
        }
        // On pressing Backspace delete last pole
        else if (SoKeyboardEvent::BACKSPACE == key && pressed) {
            // when mouse is pressed we are in a transitional state so don't mess with it
            if (MOUSE_PRESSED == MousePressMode) {
                return;
            }

            // can only delete last pole if it exists
            if (STATUS_SEEK_FIRST_CONTROLPOINT == Mode || STATUS_CLOSE == Mode) {
                return;
            }

            // if only first pole exists it's equivalent to canceling current spline
            if (poleGeoIds.size() == 1) {
                // this also exits b-spline creation if continuous mode is off
                this->quit();
                return;
            }

            // reverse the steps of press/release button
            try {
                // already ensured that CurrentConstraint == EditCurve.size() > 1
                const int delGeoId = poleGeoIds.back();
                const auto& constraints =
                    static_cast<Sketcher::SketchObject*>(sketchgui->getObject())
                        ->Constraints.getValues();
                for (int i = constraints.size() - 1; i >= 0; --i) {
                    if (delGeoId == constraints[i]->First || delGeoId == constraints[i]->Second
                        || delGeoId == constraints[i]->Third) {
                        Gui::cmdAppObjectArgs(sketchgui->getObject(), "delConstraint(%d)", i);
                    }
                }

                // Remove pole
                Gui::cmdAppObjectArgs(sketchgui->getObject(), "delGeometry(%d)", delGeoId);

                static_cast<Sketcher::SketchObject*>(sketchgui->getObject())->solve();

                poleGeoIds.pop_back();
                BSplinePoles.pop_back();

                // last entry is kept, as it corresponds to the current pole, but the one
                // corresponding to the erased pole is removed
                sugConstr.erase(std::prev(std::prev(sugConstr.end())));


                // run this in the end to draw lines and position text
                drawBSplineToPosition(prevCursorPosition);
                drawCursorToPosition(prevCursorPosition);
            }
            catch (const Base::Exception&) {
                Gui::NotifyError(sketchgui,
                                 QT_TRANSLATE_NOOP("Notifications", "Error"),
                                 QT_TRANSLATE_NOOP("Notifications", "Error deleting last pole"));
                // some commands might have already deleted some constraints/geometries but not
                // others
                Gui::Command::abortCommand();

                static_cast<Sketcher::SketchObject*>(sketchgui->getObject())->solve();

                return;
            }
        }
        // TODO: On pressing, say, W, modify last pole's weight
        // TODO: On pressing, say, M, modify next knot's multiplicity
        else {
            DrawSketchHandler::registerPressedKey(pressed, key);
        }

        return;
    }

    void quit() override
    {
        // We must see if we need to create a B-spline before cancelling everything
        // and now just like any other Handler,

        ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
            "User parameter:BaseApp/Preferences/Mod/Sketcher");

        bool continuousMode = hGrp->GetBool("ContinuousCreationMode", true);

        if (poleGeoIds.size() > 1) {
            // create B-spline from existing poles
            Mode = STATUS_CLOSE;
            finishCommand(Base::Vector2d(0.f, 0.f));
        }
        else if (poleGeoIds.size() == 1) {
            // if we just have one point and we can not close anything, then cancel this creation
            // but continue according to continuous mode
            // sketchgui->getDocument()->undo(1);

            Gui::Command::abortCommand();

            tryAutoRecomputeIfNotSolve(
                static_cast<Sketcher::SketchObject*>(sketchgui->getObject()));

            if (!continuousMode) {
                DrawSketchHandler::quit();
            }
            else {
                // This code disregards existing data and enables the continuous creation mode.
                resetHandlerState();
            }
        }
        else {  // we have no data (CurrentConstraint == 0) so user when right-clicking really wants
                // to exit
            DrawSketchHandler::quit();
        }
    }

private:
    void resetHandlerState()
    {
        Mode = STATUS_SEEK_FIRST_CONTROLPOINT;
        applyCursor();

        SplineDegree = 3;

        sugConstr.clear();
        poleGeoIds.clear();
        BSplinePoles.clear();

        eraseEditCurve();

        addSugConstraint();

        IsClosed = false;
    }

    QString getCrosshairCursorSVGName() const override
    {
        if (SketcherGui::DrawSketchHandlerBSpline::ConstrMethod == 1) {
            return QString::fromLatin1("Sketcher_Pointer_Create_Periodic_BSpline");
        }
        else {
            return QString::fromLatin1("Sketcher_Pointer_Create_BSpline");
        }
    }

    void addSugConstraint()
    {
        std::vector<AutoConstraint> sugConstr1;
        sugConstr.push_back(std::move(sugConstr1));
    }

    void drawControlPolygonToPosition(Base::Vector2d position)
    {
        std::vector<Base::Vector2d> editcurve(BSplinePoles);
        editcurve.push_back(position);

        drawEdit(editcurve);
    }

    void drawBSplineToPosition(Base::Vector2d position)
    {
        std::vector<Base::Vector3d> editcurve;
        for (auto& pole : BSplinePoles) {
            editcurve.emplace_back(pole.x, pole.y, 0.0);
        }
        editcurve.emplace_back(position.x, position.y, 0.0);
        size_t degree = std::min(editcurve.size() - 1, static_cast<size_t>(SplineDegree));
        bool periodic = (ConstrMethod != 0);

        std::vector<double> weights(editcurve.size(), 1.0);
        std::vector<double> knots;
        std::vector<int> mults;
        if (!periodic) {
            for (size_t i = 0; i < editcurve.size() - degree + 1; ++i) {
                knots.push_back(i);
            }
            mults.resize(editcurve.size() - degree + 1, 1);
            mults.front() = degree + 1;
            mults.back() = degree + 1;
        }
        else {
            for (size_t i = 0; i < editcurve.size() + 1; ++i) {
                knots.push_back(i);
            }
            mults.resize(editcurve.size() + 1, 1);
        }

        // TODO: This maybe optimized by storing the spline as an attribute.
        Part::GeomBSplineCurve editBSpline(editcurve, weights, knots, mults, degree, periodic);
        editBSpline.setPoles(editcurve);

        std::vector<Part::Geometry*> editBSplines;
        editBSplines.push_back(&editBSpline);

        drawEdit(editBSplines);
    }

    void drawCursorToPosition(Base::Vector2d position)
    {
        if (!BSplinePoles.empty()) {
            float length = (position - BSplinePoles.back()).Length();
            float angle = (position - BSplinePoles.back()).GetAngle(Base::Vector2d(1.f, 0.f));

            if (showCursorCoords()) {
                SbString text;
                std::string lengthString = lengthToDisplayFormat(length, 1);
                std::string angleString =
                    angleToDisplayFormat((angle != -FLOAT_MAX) ? angle * 180 / M_PI : 0, 1);
                text.sprintf(" (%s, %s)", lengthString.c_str(), angleString.c_str());
                setPositionText(position, text);
            }
        }
    }

    void eraseEditCurve()
    {
        drawEdit(std::vector<Base::Vector2d>());
    }

    bool finishCommand(Base::Vector2d position)
    {
        if (Mode == STATUS_CLOSE) {
            unsetCursor();
            resetPositionText();

            std::stringstream stream;

            for (auto& pole : BSplinePoles) {
                stream << "App.Vector(" << pole.x << "," << pole.y << "),";
            }

            std::string controlpoints = stream.str();

            // remove last comma and add brackets
            int index = controlpoints.rfind(',');
            controlpoints.resize(index);

            controlpoints.insert(0, 1, '[');
            controlpoints.append(1, ']');

            int currentgeoid = getHighestCurveIndex();

            unsigned int maxDegree =
                ConstrMethod == 0 ? (BSplinePoles.size() - 1) : (BSplinePoles.size());

            try {
                // Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add B-spline curve"));

                /*Gui::cmdAppObjectArgs(sketchgui->getObject(), "addGeometry(Part.BSplineCurve"
                    "(%s,%s),"
                    "%s)",
                        controlpoints.c_str(),
                        ConstrMethod == 0 ?"False":"True",
                        geometryCreationMode==Construction?"True":"False"); */

                // {"poles", "mults", "knots", "periodic", "degree", "weights", "CheckRational",
                // NULL};
                Gui::cmdAppObjectArgs(sketchgui->getObject(),
                                      "addGeometry(Part.BSplineCurve"
                                      "(%s,None,None,%s,%d,None,False),%s)",
                                      controlpoints.c_str(),
                                      ConstrMethod == 0 ? "False" : "True",
                                      std::min(maxDegree, SplineDegree),
                                      constructionModeAsBooleanText());

                currentgeoid++;

                // autoconstraints were added to the circles of the poles, which is ok because they
                // must go to the right position, or the user will freak-out if they appear out of
                // the autoconstrained position. However, autoconstraints on the first and last
                // pole, in normal non-periodic b-splines (with appropriate endpoint knot
                // multiplicity) as the ones created by this tool are intended for the b-spline
                // endpoints, and not for the poles, so here we retrieve any autoconstraint on those
                // poles' center and mangle it to the endpoint.
                if (ConstrMethod == 0) {
                    for (auto& constr : static_cast<Sketcher::SketchObject*>(sketchgui->getObject())
                                            ->Constraints.getValues()) {
                        if (constr->First == poleGeoIds[0]
                            && constr->FirstPos == Sketcher::PointPos::mid) {
                            constr->First = currentgeoid;
                            constr->FirstPos = Sketcher::PointPos::start;
                        }
                        else if (constr->First == poleGeoIds.back()
                                 && constr->FirstPos == Sketcher::PointPos::mid) {
                            constr->First = currentgeoid;
                            constr->FirstPos = Sketcher::PointPos::end;
                        }
                    }
                }

                // Constraint pole circles to B-spline.
                std::stringstream cstream;

                cstream << "conList = []\n";

                for (size_t i = 0; i < poleGeoIds.size(); i++) {
                    cstream << "conList.append(Sketcher.Constraint('InternalAlignment:Sketcher::"
                               "BSplineControlPoint',"
                            << poleGeoIds[0] + i << "," << static_cast<int>(Sketcher::PointPos::mid)
                            << "," << currentgeoid << "," << i << "))\n";
                }

                cstream << Gui::Command::getObjectCmd(sketchgui->getObject())
                        << ".addConstraint(conList)\n";
                cstream << "del conList\n";

                Gui::Command::doCommand(Gui::Command::Doc, cstream.str().c_str());

                // for showing the knots on creation
                Gui::cmdAppObjectArgs(sketchgui->getObject(),
                                      "exposeInternalGeometry(%d)",
                                      currentgeoid);
            }
            catch (const Base::Exception&) {
                Gui::NotifyError(sketchgui,
                                 QT_TRANSLATE_NOOP("Notifications", "Error"),
                                 QT_TRANSLATE_NOOP("Notifications", "Error creating B-spline"));
                Gui::Command::abortCommand();

                tryAutoRecomputeIfNotSolve(
                    static_cast<Sketcher::SketchObject*>(sketchgui->getObject()));

                return false;
            }

            Gui::Command::commitCommand();

            tryAutoRecomputeIfNotSolve(
                static_cast<Sketcher::SketchObject*>(sketchgui->getObject()));

            ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
                "User parameter:BaseApp/Preferences/Mod/Sketcher");
            bool continuousMode = hGrp->GetBool("ContinuousCreationMode", true);

            if (continuousMode) {
                // This code enables the continuous creation mode.
                resetHandlerState();

                drawCursorToPosition(position);

                /* It is ok not to call to purgeHandler
                 * in continuous creation mode because the
                 * handler is destroyed by the quit() method on pressing the
                 * right button of the mouse */
            }
            else {
                sketchgui->purgeHandler();  // no code after this line, Handler get deleted in
                                            // ViewProvider
            }
        }
        else {
            drawCursorToPosition(position);
        }

        return true;
    }

protected:
    SELECT_MODE Mode;
    MOUSE_PRESS_MODE MousePressMode;

    // Stores position of the poles of the BSpline.
    std::vector<Base::Vector2d> BSplinePoles;

    // suggested autoconstraints for poles.
    // A new one must be added e.g. using addSugConstraint() before adding a new pole.
    std::vector<std::vector<AutoConstraint>> sugConstr;

    int ConstrMethod;
    unsigned int SplineDegree;
    bool IsClosed;
    std::vector<int> poleGeoIds;
    Base::Vector2d prevCursorPosition;
};


}  // namespace SketcherGui


#endif  // SKETCHERGUI_DrawSketchHandlerBSpline_H
